/*
 * Copyright (C) 2010 M. Hautle.
 * 
 * This file is part of TrackedBean.
 * 
 * TrackedBean is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free
 * Software Foundation, either version 3 of the License, or (at your option) any later version.
 * 
 * TrackedBean is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
 * A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public License along with TrackedBean. If not, see <http://www.gnu.org/licenses/>.
 */
package ch.trackedbean.tracking;

import static ch.trackedbean.tracking.internal.injection.InjectionUtils.*;
import java.io.*;
import org.junit.runner.*;
import org.junit.runner.notification.*;
import org.junit.runners.*;
import org.junit.runners.model.*;
import ch.trackedbean.tracking.injection.*;
import ch.trackedbean.tracking.internal.*;
import ch.trackedbean.tracking.internal.injection.*;

/**
 * JUnit test runner using {@link TrackedBean} weaving.
 * 
 * @author M. Hautle
 * @since 18.12.2010
 */
public class TrackedRunner extends Runner {
    /** The delegate used for the real work. */
    private final BlockJUnit4ClassRunner delegate;

    /**
     * Default constructor.
     * 
     * @param klass The test class
     * @throws InitializationError If something went wrong
     */
    public TrackedRunner(Class<?> klass) throws InitializationError {
        try {
            delegate = new BlockJUnit4ClassRunner(new TrackedLoader(klass.getClassLoader()).loadClass(klass.getName()));
        } catch (ClassNotFoundException e) {
            throw new InitializationError(e);
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Description getDescription() {
        return delegate.getDescription();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void run(RunNotifier notifier) {
        delegate.run(notifier);
    }

    /**
     * Somewhat modified class loader for {@link TrackedBean}s.
     * 
     * @author M. Hautle
     * @since 18.12.2010
     */
    private static class TrackedLoader extends ClassLoader {
        /** The class analyzer to use. */
        private final AbstractClassAnalyzer analyzer;

        /** The class file adapter. */
        private final TrackedBeanAdapter adapter;

        /**
         * Default constructor.
         * 
         * @param parent The parent classloader
         */
        public TrackedLoader(ClassLoader parent) {
            super(parent);
            analyzer = new AbstractClassAnalyzer("ch.trackedbean.tracking.TrackedBean") {
                @Override
                protected InputStream getResourceStream(String name) throws Exception {
                    return getResourceAsStream(name);
                }
            };
            adapter = new TrackedBeanAdapter(analyzer);
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public synchronized Class<?> loadClass(final String name, final boolean resolve) throws ClassNotFoundException {
            // First, check if the class has already been loaded
            Class c = findLoadedClass(name);
            if (c == null) {
                // load java and junit classes via original class loader
                if (name.startsWith("java.") || name.startsWith("org.junit."))
                    return super.loadClass(name, resolve);
                try {
                    final String internalName = name.replace('.', '/');
                    // gets an input stream to read the bytecode of the class
                    final String resource = internalName.concat(".class");
                    // read the class and adapt it if necessary
                    final byte[] b = adapt(readClass(getResourceAsStream(resource)), internalName);
                    c = defineClass(name, b, 0, b.length);
                } catch (final Exception e) {
                    // e.printStackTrace();
                    throw new ClassNotFoundException(name, e);
                }
            }
            if (resolve)
                resolveClass(c);
            return c;
        }

        /**
         * Adapts the passed class byte code if necessary.
         * 
         * @param b A byte array holding the class file
         * @param name The internal class name
         * @return The adapted bytecode
         * @throws IOException If something went wrong
         */
        private byte[] adapt(byte[] b, String name) throws IOException {
            final ClassInformation ci = analyzer.getInformation(name, b);
            if (ci.tracked || ci.parentTracked)
                return adapter.adapt(ci, b);
            return b;
        }
    }
}
